#include "DirWatcher.h"




#include "DirWatcher.h"

#include "Poco/Exception.h"

#include "Poco/Path.h"
#include "Poco/Glob.h"
#include "Poco/DirectoryIterator.h"
#include "Poco/Event.h"
#include "Poco/Exception.h"
#include "Poco/Buffer.h"
#if defined(POCO_WIN32_UTF8)
#include "Poco/UnicodeConverter.h"
#endif
#include <algorithm>
#include <map>


using Poco::SystemException;
using Poco::Timestamp;
using Poco::DirectoryIterator;
using Poco::FileImpl;

class DirWatcherStrategy
{
public:
	DirWatcherStrategy(DirWatcher& owner) :
		_owner(owner)
	{
	}

	virtual ~DirWatcherStrategy()
	{
	}

	DirWatcher& owner()
	{
		return _owner;
	}

	virtual void run() = 0;
	virtual void stop() = 0;
	virtual bool supportsMoveEvents() const = 0;

protected:
	struct ItemInfo
	{
		ItemInfo() :
			size(0)
		{
		}

		ItemInfo(const ItemInfo& other) :
			path(other.path),
			size(other.size),
			lastModified(other.lastModified)
		{
		}

		explicit ItemInfo(const File& f) :
			path(f.path()),
			size(f.isFile() ? f.getSize() : 0),
			lastModified(f.getLastModified())
		{
		}

		std::string path;
		File::FileSize size;
		Timestamp lastModified;
	};
	typedef std::map<std::string, ItemInfo> ItemInfoMap;

	void scan(ItemInfoMap& entries)
	{
		DirectoryIterator it(owner().directory());
		DirectoryIterator end;
		while (it != end)
		{
			entries[it.path().getFileName()] = ItemInfo(*it);
			++it;
		}
	}

	void compare(ItemInfoMap& oldEntries, ItemInfoMap& newEntries)
	{
		for (ItemInfoMap::iterator itn = newEntries.begin(); itn != newEntries.end(); ++itn)
		{
			ItemInfoMap::iterator ito = oldEntries.find(itn->first);
			if (ito != oldEntries.end())
			{
				if ((owner().eventMask() & DirWatcher::DW_ITEM_MODIFIED) && !owner().eventsSuspended())
				{
					if (itn->second.size != ito->second.size || itn->second.lastModified != ito->second.lastModified)
					{
						Poco::File f(itn->second.path);
						DirWatcher::DirectoryEvent ev(f, DirWatcher::DW_ITEM_MODIFIED);
						owner().itemModified(&owner(), ev);
					}
				}
				oldEntries.erase(ito);
			}
			else if ((owner().eventMask() & DirWatcher::DW_ITEM_ADDED) && !owner().eventsSuspended())
			{
				Poco::File f(itn->second.path);
				DirWatcher::DirectoryEvent ev(f, DirWatcher::DW_ITEM_ADDED);
				owner().itemAdded(&owner(), ev);
			}
		}
		if ((owner().eventMask() & DirWatcher::DW_ITEM_REMOVED) && !owner().eventsSuspended())
		{
			for (ItemInfoMap::iterator it = oldEntries.begin(); it != oldEntries.end(); ++it)
			{
				Poco::File f(it->second.path);
				DirWatcher::DirectoryEvent ev(f, DirWatcher::DW_ITEM_REMOVED);
				owner().itemRemoved(&owner(), ev);
			}
		}
	}

private:
	DirWatcherStrategy();
	DirWatcherStrategy(const DirWatcherStrategy&);
	DirWatcherStrategy& operator = (const DirWatcherStrategy&);

	DirWatcher& _owner;
};



void handleLastErrorImpl(const std::string& path)
{
	DWORD err = GetLastError();
	switch (err)
	{
	case ERROR_FILE_NOT_FOUND:
		throw Poco::FileNotFoundException(path, err);
	case ERROR_PATH_NOT_FOUND:
	case ERROR_BAD_NETPATH:
	case ERROR_CANT_RESOLVE_FILENAME:
	case ERROR_INVALID_DRIVE:
		throw Poco::PathNotFoundException(path, err);
	case ERROR_ACCESS_DENIED:
		throw Poco::FileAccessDeniedException(path, err);
	case ERROR_ALREADY_EXISTS:
	case ERROR_FILE_EXISTS:
		throw Poco::FileExistsException(path, err);
	case ERROR_INVALID_NAME:
	case ERROR_DIRECTORY:
	case ERROR_FILENAME_EXCED_RANGE:
	case ERROR_BAD_PATHNAME:
		throw Poco::PathSyntaxException(path, err);
	case ERROR_FILE_READ_ONLY:
		throw Poco::FileReadOnlyException(path, err);
	case ERROR_CANNOT_MAKE:
		throw Poco::CreateFileException(path, err);
	case ERROR_DIR_NOT_EMPTY:
		throw Poco::FileException("directory not empty", path, err);
	case ERROR_WRITE_FAULT:
		throw Poco::WriteFileException(path, err);
	case ERROR_READ_FAULT:
		throw Poco::ReadFileException(path, err);
	case ERROR_SHARING_VIOLATION:
		throw Poco::FileException("sharing violation", path, err);
	case ERROR_LOCK_VIOLATION:
		throw Poco::FileException("lock violation", path, err);
	case ERROR_HANDLE_EOF:
		throw Poco::ReadFileException("EOF reached", path, err);
	case ERROR_HANDLE_DISK_FULL:
	case ERROR_DISK_FULL:
		throw Poco::WriteFileException("disk is full", path, err);
	case ERROR_NEGATIVE_SEEK:
		throw Poco::FileException("negative seek", path, err);
	default:
		throw Poco::FileException(path, err);
	}
}

class WindowsDirWatcherStrategy : public DirWatcherStrategy
{
public:
	WindowsDirWatcherStrategy(DirWatcher& owner) :
		DirWatcherStrategy(owner)
	{
		_hStopped = CreateEventW(NULL, FALSE, FALSE, NULL);
		if (!_hStopped)
			throw SystemException("cannot create event");
	}

	~WindowsDirWatcherStrategy()
	{
		CloseHandle(_hStopped);
	}

	void run()
	{
		ItemInfoMap entries;
		scan(entries);

		DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME;
		if (owner().eventMask() & DirWatcher::DW_ITEM_MODIFIED)
			filter |= FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE;

		std::string path(owner().directory().path());
#if defined(POCO_WIN32_UTF8)
		std::wstring upath;
		Poco::UnicodeConverter::toUTF16(path.c_str(), upath);
		HANDLE hChange = FindFirstChangeNotificationW(upath.c_str(), TRUE, filter);
#else
		HANDLE hChange = FindFirstChangeNotificationA(path.c_str(), TRUE, filter);
#endif

		if (hChange == INVALID_HANDLE_VALUE)
		{
			try
			{
				handleLastErrorImpl(path);
			}
			catch (Poco::Exception& exc)
			{
				owner().scanError(&owner(), exc);
			}
			return;
		}

		bool stopped = false;
		while (!stopped)
		{
			try
			{
				HANDLE h[2];
				h[0] = _hStopped;
				h[1] = hChange;
				switch (WaitForMultipleObjects(2, h, FALSE, INFINITE))
				{
				case WAIT_OBJECT_0:
					stopped = true;
					break;
				case WAIT_OBJECT_0 + 1:
				{
					ItemInfoMap newEntries;
					scan(newEntries);
					compare(entries, newEntries);
					std::swap(entries, newEntries);
					if (FindNextChangeNotification(hChange) == FALSE)
					{
						handleLastErrorImpl(path);
					}
				}
				break;
				default:
					throw SystemException("failed to wait for directory changes");
				}
			}
			catch (Poco::Exception& exc)
			{
				owner().scanError(&owner(), exc);
			}
		}
		FindCloseChangeNotification(hChange);
	}

	void stop()
	{
		SetEvent(_hStopped);
	}

	bool supportsMoveEvents() const
	{
		return false;
	}

private:
	HANDLE _hStopped;
};






DirWatcher::DirWatcher(const std::string& path, int eventMask, int scanInterval) :
	_directory(path),
	_eventMask(eventMask),
	_scanInterval(scanInterval)
{
	init();
}


DirWatcher::DirWatcher(const Poco::File& directory, int eventMask, int scanInterval) :
	_directory(directory),
	_eventMask(eventMask),
	_scanInterval(scanInterval)
{
	init();
}


DirWatcher::~DirWatcher()
{
	try
	{
		stop();
		delete _pStrategy;
	}
	catch (...)
	{
		poco_unexpected();
	}
}


void DirWatcher::suspendEvents()
{
	poco_assert(_eventsSuspended > 0);

	_eventsSuspended--;
}


void DirWatcher::resumeEvents()
{
	_eventsSuspended++;
}


void DirWatcher::init()
{
	if (!_directory.exists())
		throw Poco::FileNotFoundException(_directory.path());

	if (!_directory.isDirectory())
		throw Poco::InvalidArgumentException("not a directory", _directory.path());

#if POCO_OS == POCO_OS_WINDOWS_NT
	_pStrategy = new WindowsDirWatcherStrategy(*this);
#elif POCO_OS == POCO_OS_LINUX
	_pStrategy = new LinuxDirectoryWatcherStrategy(*this);
#elif POCO_OS == POCO_OS_MAC_OS_X || POCO_OS == POCO_OS_FREE_BSD
	_pStrategy = new BSDDirectoryWatcherStrategy(*this);
#else
	_pStrategy = new PollingDirectoryWatcherStrategy(*this);
#endif
	_thread.start(*this);
}


void DirWatcher::run()
{
	_pStrategy->run();
}


void DirWatcher::stop()
{
	_pStrategy->stop();
	_thread.join();
}


bool DirWatcher::supportsMoveEvents() const
{
	return _pStrategy->supportsMoveEvents();
}

